﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser.DOM.CSS.SAC
{
    #region Using Statements

    using System;
    using System.Linq;
    using System.Text;
    using System.Collections.Generic;
    using System.Text.RegularExpressions;
    using KLF.Lib.Browser.DOM.CSS.SAC.Selectors;
    using KLF.Lib.Browser.DOM.CSS.SAC.Conditions;

    #endregion

    public class SACParser
    {
        /// <summary>
        /// A regex for parsing out css text.
        /// </summary>
        static Regex RuleMatcher = new Regex(
            @"(?<cssComment>(?:\/\*).*?(?:\*\/))|" +
            @"(?<cssMedia>\@media)|" +
            @"(?<cssImport>\@import\s+url\s*[\(""]+(?<cssImportUrl>[^""\)]+)[""\)]+)|" +
            @"(?<cssText>([a-zA-Z0-9]+\\\:)?[a-zA-Z0-9\-\=\<\>\[\]\*\|\.\s\(\)#~\+=""']+)|" +
            @"(?<cssSel>[:]{1,2}(enabled|disabled|checked|root|nth|first|last|only|empty|not)[a-zA-Z0-9\.\-_\(\)\+\s]*)|" +
            @"(?<cssPriority>\!important)|" +
            @"(?<cssColon>[:]{1})|" +
            @"(?<cssStyleOpen>[\{])|" +
            @"(?<cssStyleClose>[\{])|" +
            @"(?<cssPropertySeparator>[\;])|" +
            @"(?<cssSelectorSeparator>[\,])",
            RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);

        CSSParserContext Context { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public List<CSSParserToken> Parse(string source)
        {
            Context = new CSSParserContext(source);

            while (!Context.EndOfString)
            {
                Match match = RuleMatcher.Match(Context.CSSText, Context.Index);

                if (match.Success)
                {
                    CSSParserToken token = null;

                    if (match.Groups["cssComment"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssComment,
                            match.Groups["cssComment"].Value);
                    }
                    else if (match.Groups["cssStyleOpen"].Success)
                    {
                        Context.InStyle = true;
                        token = new CSSParserToken(
                            CSSParserTokenType.CssStyleOpen,
                            match.Groups["cssStyleOpen"].Value);
                    }
                    else if (match.Groups["cssStyleClose"].Success)
                    {
                        Context.InStyle = false;
                        token = new CSSParserToken(
                            CSSParserTokenType.CssStyleClose,
                            match.Groups["cssStyleClose"].Value);
                    }
                    else if (match.Groups["cssPropertySeparator"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssPropertySeperator,
                            match.Groups["cssPropertySeparator"].Value);
                    }
                    else if (match.Groups["cssSelectorSeparator"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssSelectorSeparator,
                            match.Groups["cssSelectorSeparator"].Value);
                    }
                    else if (match.Groups["cssText"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssText,
                            match.Groups["cssText"].Value);
                    }
                    else if (match.Groups["cssSel"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssText,
                            match.Groups["cssSel"].Value);
                    }
                    else if (match.Groups["cssImport"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssImport,
                            match.Groups["cssImport"].Value);
                    }
                    else if (match.Groups["cssMedia"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssMedia,
                            match.Groups["cssMedia"].Value);
                    }
                    else if (match.Groups["cssPriority"].Success)
                    {
                        token = new CSSParserToken(
                            CSSParserTokenType.CssPriority,
                            match.Groups["cssPriority"].Value);
                    }
                    Context.Index = match.Index + token.RawText.Length;
                    token.RawText = token.RawText.Trim();

                    if (token.Type == CSSParserTokenType.CssImport)
                        token.RawText = match.Groups["cssImportUrl"].Value;

                    if (token != null && token.RawText.Length > 0)
                    {

                        int index = token.RawText.IndexOf(':');
                        if (index > -1 && Context.InStyle)
                        {
                            string propertyName = token.RawText.Substring(0, index).Trim();
                            string propertyValue = token.RawText.Substring(index + 1).Trim();

                            Context.Tokens.Add(new CSSParserToken(CSSParserTokenType.CssPropertyName, propertyName));
                            Context.Tokens.Add(new CSSParserToken(CSSParserTokenType.CssPropertyValue, propertyValue));
                        }
                        else
                        {
                            Context.Tokens.Add(token);
                        }
                    }
                }
                else
                {
                    var str = Context.CSSText.Substring(Context.Index);
                    Context.Tokens.Add(new CSSParserToken(CSSParserTokenType.CssText, str));
                    return Context.Tokens;
                }
            }
            return Context.Tokens;
        }

        /// <summary>
        /// Parse a CSS property value.
        /// </summary>
        /// <param name="source"></param>
        public void ParsePropertyValue(string source)
        {
        }

        /// <summary>
        /// Parse a CSS priority value (e.g. "!important").
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public bool ParsePriority(string source)
        {
            return false;
        }

        /// <summary>
        /// Parse a CSS rule.
        /// </summary>
        /// <param name="source"></param>
        public void ParseRule(string source)
        {
        }

        static Regex SelectorRegex = new Regex(
            @"\s*(?<attr>\[(?<attrName>[a-zA-Z0-9_\-\:]+){1}"
                + @"\s*(?<attrOperator>~=|\|=|\^=|\$=|\*=|=){0,1}"
                + @"\s*[""']*(?<attrValue>[^\]]+){0,1}[""']*\])|" +
            @"\s*(?<childSelector>[\>]{1})|" +
            @"\s*(?<adjacentSelector>[\+]{1})|" +
            @"\s*(?<siblingSelector>[~]{1})|" +
            @"\s*(?<anySelector>[\*]{1})|" +
            @"(?<classCondition>\.[a-zA-Z0-9_]+){1}|" +
            @"\s*(?<pseudoClass>[:]{1,2}[a-zA-Z0-9\-\(\)\+]+)|" +
            @"\s*(?<selector>" +
                @"(?<tagName>(?<tagNamespace>[a-zA-Z0-9]+\\:){0,1}[a-zA-Z0-9_]+){0,1}" +
                @"(?<tagClass>\.[a-zA-Z0-9_\.\-]+){0,1}" +
                @"(?<tagId>#[a-zA-Z0-9_\.\-]+)?)",
            RegexOptions.CultureInvariant |
            RegexOptions.IgnoreCase |
            RegexOptions.ExplicitCapture);

        /// <summary>
        /// Parse a comma separated list of selectors.
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        public Selector ParseSelector(string source)
        {
            var result = Parse(source).Where(t => t.Type == CSSParserTokenType.CssText);
            List<Selector> selectors = new List<Selector>();
            Selector latestSelector = null;

            foreach (CSSParserToken token in result)
            {
                MatchCollection mc = SelectorRegex.Matches(token.CleanText);
                Selector current = null;

                foreach (Match m in mc)
                {
                    if (m.Groups["selector"].Success)
                    {
                        if (m.Groups["selector"].Value.Length == 0)
                            continue;

                        ElementSelector selector = new ElementSelector();

                        if (m.Groups["tagName"].Success)
                            selector.ElementName = m.Groups["tagName"].Value.Replace("\\:", ":");
                        else
                            selector = ElementSelector.Any;

                        if (m.Groups["tagClass"].Success)
                            selector.AddCondition(
                                new ClassCondition(m.Groups["tagClass"].Value));

                        if (m.Groups["tagId"].Success)
                            selector.AddCondition(
                                new IdCondition(m.Groups["tagId"].Value));

                        if (current != null && current is BinarySelector)
                        {
                            if (((BinarySelector)current).Right != null)
                                current = (new DescendantSelector(current, null));
                            ((BinarySelector)current).Right = selector;
                        }
                        else if (latestSelector != null)
                        {
                            current = (new DescendantSelector(latestSelector, selector));
                        }
                        latestSelector = selector;
                    }
                    else if (m.Groups["adjacentSelector"].Success)
                    {
                        // A + B + 
                        current = new AdjacentSelector(latestSelector, null);
                    }
                    else if (m.Groups["anySelector"].Success)
                    {
                        ElementSelector a = ElementSelector.Any;

                        if (current != null && current is BinarySelector)
                        {
                            if (((BinarySelector)current).Right != null)
                                current = (new DescendantSelector(current, null));
                            ((BinarySelector)current).Right = a;
                        }
                        else if (latestSelector != null)
                        {
                            current = (new DescendantSelector(latestSelector, a));
                        }
                        latestSelector = a;
                    }
                    else if (m.Groups["childSelector"].Success)
                    {
                        // A > B
                        current = new ChildSelector(current ?? latestSelector, null);
                    }
                    else if (m.Groups["siblingSelector"].Success)
                    {
                        current = new SiblingSelector(current ?? latestSelector, null);
                    }
                    else
                    {
                        if (latestSelector == null)
                            latestSelector = ElementSelector.Any;

                        if (m.Groups["attr"].Success)
                        {
                            latestSelector.AddCondition(new AttributeCondition(
                                m.Groups["attrName"].Value,
                                m.Groups["attrOperator"].Success ?
                                    m.Groups["attrOperator"].Value : "exists",
                                m.Groups["attrValue"].Success ?
                                    m.Groups["attrValue"].Value : ""));

                        }
                        else if(m.Groups["classCondition"].Success)
                        {
                            latestSelector.AddCondition(
                                new ClassCondition(m.Groups["classCondition"].Value));
                        }
                        else if (m.Groups["pseudoClass"].Success)
                        {
                            Condition cond = PseudoFactory.CreatePseudoCondition(m.Groups["pseudoClass"].Value);

                            if (cond != null)
                                latestSelector.AddCondition(cond);
                        }
                    }
                }
                Selector newSelector = current ?? latestSelector;

                if (!selectors.Contains(newSelector))
                    selectors.Add(newSelector);
            }

            switch (selectors.Count)
            {
                case 0: return null;
                case 1: return selectors[0];
                default: return new OrSelector(selectors);
            }
        }

        /// <summary>
        /// Parse a CSS style declaration (without '{' and '}').
        /// </summary>
        /// <param name="source"></param>
        public void ParseStyleDeclaration(string source)
        {
        }

        /// <summary>
        /// Parse a CSS document.
        /// </summary>
        /// <param name="source"></param>
        public void ParseStyleSheet(string source)
        {
        }

        /// <summary>
        /// Parse a CSS document from a URI.
        /// </summary>
        /// <param name="source"></param>
        public void ParseStyleSheet(Uri source)
        {
        }
    }

    /// <summary>
    /// What type of token is it.
    /// </summary>
    public enum CSSParserTokenType
    {
        CssComment,
        CssMedia,
        CssImport,
        CssStyleOpen,
        CssStyleClose,
        CssPriority,
        CssPropertyName,
        CssPropertyValue,
        CssAttributeSelector,
        CssSelectorSeparator,
        CssPropertySeperator,
        CssText
    }

    /// <summary>
    /// A token read from the css stream.
    /// </summary>
    public class CSSParserToken
    {
        static Regex wsCleanup = new Regex(@"[\s]+");

        public CSSParserToken(CSSParserTokenType _type, String _raw)
        {
            RawText = _raw;
            Type = _type;
        }

        public string RawText { get; set; }

        public string CleanText
        {
            get
            {
                return wsCleanup.Replace(RawText.Trim(), " ");
            }
        }

        public CSSParserTokenType Type { get; set; }

        public override string ToString()
        {
            return CleanText;
        }
    }

    public class CSSParserContext
    {
        internal CSSParserContext(string css)
        {
            CSSText = css;
            Tokens = new List<CSSParserToken>();
        }

        public string CSSText { get; private set; }
        public int Index { get; set; }
        public List<CSSParserToken> Tokens { get; set; }
        public bool EndOfString { get { return Index >= CSSText.Length; } }
        public bool InStyle { get; set; }
    }
}
